package com.reeygu.item.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.reeygu.item.annotation.AccessLimit;
import com.reeygu.item.pojo.Order;
import com.reeygu.item.pojo.SeckillOrder;
import com.reeygu.item.pojo.User;
import com.reeygu.item.rabbitMQ.MQSender;
import com.reeygu.item.service.GoodsService;
import com.reeygu.item.service.OrderService;
import com.reeygu.item.service.SeckillGoodsService;
import com.reeygu.item.service.SeckillOrderService;
import com.reeygu.item.utils.JsonUtil;
import com.reeygu.item.vo.GoodsVo;
import com.reeygu.item.vo.RespBean;
import com.reeygu.item.vo.RespBeanEnum;
import com.reeygu.item.vo.SeckillMessage;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import springfox.documentation.annotations.ApiIgnore;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


@Slf4j
@Controller
@RequestMapping("/swagger2/seckill")
@Api(tags = "SeckillController", description = "秒杀接口")
public class SeckillController implements InitializingBean {
    @Autowired
    private GoodsService goodsService;
    @Autowired
    private SeckillOrderService seckillOrderService;
    @Autowired
    private SeckillGoodsService seckillGoodsService;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private MQSender mqSender;
    @Autowired
    private OrderService orderService;

    //本地库存校验，当库存为零时，没必要再访问接口和redis了，可通过本地标注
    private Map<Long, Boolean> emptyStockMap = new HashMap<>();

    /**
     * 前后端融合：普通的下单秒杀操作，会出现超卖
     */
    @RequestMapping("/doSeckill")
    public String doSeckill(Model model, User user, Long goodsId) {
        if(user == null) {
            log.warn("用户信息为空");
            return "login";
        }
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
        //库存不足够
        if(goodsVo.getStockCount() < 1) {
            model.addAttribute("errmsg", RespBeanEnum.EMPTY_STOCK);
            log.warn("用户{}在{}时刻购物时库存不足，商品：{}已售空", user.toString(), new Date(), goodsId);
            return "seckillFail";
        }
        //用户是否重复抢购
        SeckillOrder seckillOrder = seckillOrderService.getOne(new LambdaQueryWrapper<SeckillOrder>()
                .eq(SeckillOrder::getUserId, user.getId())
                .eq(SeckillOrder::getGoodsId, goodsId));
        if(seckillOrder != null) {
            model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR);
            log.warn("用户{}在{}时刻重复抢购商品：{}", user.toString(), new Date(), goodsId);
            return "seckillFail";
        }
        //购物成功，添加订单信息
        Order order = seckillOrderService.generatorKillAndOrder(goodsVo, user);
        //商品库存-1
        int statusGoods = goodsService.deductGoods(order.getGoodsId());
        //抢购库存-1 这里简单处理，直接根据goodsId查询,正常情况是要根据 seckillGoodsId 查询的
        int statusSeckillGoods = seckillGoodsService.deductSeckillGoods(order.getGoodsId());
        if(statusGoods + statusSeckillGoods != 2) {
            model.addAttribute("errmsg", RespBeanEnum.EMPTY_STOCK);
            log.warn("用户{}在{}时刻购物时库存不足，商品：{}已售空", user.toString(), new Date(), goodsId);
            return "seckillFail";
        }
        model.addAttribute("order", order);
        model.addAttribute("goods", goodsVo);
        model.addAttribute("user", user);
        log.warn("用户{}在{}时刻成功抢购商品：{}", user.toString(), new Date(), goodsId);
        log.info("时间：{}", order.getCreatDate());
        return "orderDetail";
    }

    /**
     * 上面秒杀业务一次优化
     * 这里是模拟前后端分离，使用不返回html页面了，直接返回数据，
     * 在高并发下，库存的查看和修改要保证原子性，才能解决库存超卖问题
     */
    @ResponseBody
    @RequestMapping(value = "/doSeckill/optimize", method = RequestMethod.POST)
    public RespBean doSeckillOptimize(Model model, User user, Long goodsId) {
        if(user == null) {
            log.warn("用户信息为空");
            return RespBean.error(RespBeanEnum.ERROR);
        }
        GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
        //库存不足够
        if(goodsVo.getStockCount() < 1) {
            log.warn("用户{}在{}时刻购物时库存不足，商品：{}已售空", user.toString(), new Date(), goodsId);
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }
        //用户是否重复抢购
//        SeckillOrder seckillOrder = seckillOrderService.getOne(new LambdaQueryWrapper<SeckillOrder>()
//                .eq(SeckillOrder::getUserId, user.getId())
//                .eq(SeckillOrder::getGoodsId, goodsId));
        //由于mysql查询速度慢，所以上面代码改成下面redis查询
        SeckillOrder seckillOrder =
                ((SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId));
        if(seckillOrder != null) {
            log.warn("用户{}在{}时刻重复抢购商品：{}", user.toString(), new Date(), goodsId);
            return RespBean.error(RespBeanEnum.REPEATE_ERROR);
        }
        //购物成功，添加订单信息
        Order order = seckillOrderService.generatorKillAndOrder(goodsVo, user);
        //商品库存-1
        int statusGoods = goodsService.deductGoods(order.getGoodsId());
        //抢购库存-1 这里简单处理，直接根据goodsId查询,正常情况是要根据 seckillGoodsId 查询的
        int statusSeckillGoods = seckillGoodsService.deductSeckillGoods(order.getGoodsId());
        if(statusGoods + statusSeckillGoods != 2) {
            log.warn("用户{}在{}时刻购物时库存不足，商品：{}已售空", user.toString(), new Date(), goodsId);
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }
        model.addAttribute("order", order);
        model.addAttribute("goods", goodsVo);
        model.addAttribute("user", user);
        log.warn("用户{}在{}时刻成功抢购商品：{}", user.toString(), new Date(), goodsId);
        log.info("时间：{}", order.getCreatDate());
        //将上面3个对象封装成Vo并放到data里面返回给前端，这里不实现了
        return null;
    }

    /**
     * 上面秒杀业务二次优化
     * 使用redis+rabbitmq的方式实现流量消峰
     * ApiIgnore: 不生成api文档
     */
    @ApiIgnore
    @ResponseBody
    @RequestMapping(value = "/doSeckill/optimize/tow", method = RequestMethod.POST)
    public RespBean doSeckillOptimizeTow(Model model, User user, Long goodsId) {
        if(user == null) {
            log.warn("用户信息为空");
            return RespBean.error(RespBeanEnum.ERROR);
        }
        //内存标记,减少redis的访问
        //先本地校验,map的初始化在afterPropertiesSet方法中
        if(emptyStockMap.get(goodsId)) {
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }
        ValueOperations opt = redisTemplate.opsForValue();
        //库存不足
        //直接-1，这里的decrement具有原子性操作
        Long stock = opt.decrement("seckillGoods:" + goodsId);
        if(stock == null || stock < 0L) {
            //为了好看恢复回去+1
            opt.increment("seckillGoods:" + goodsId);
            log.warn("用户{}在{}时刻购物时库存不足，商品：{}已售空", user.toString(), new Date(), goodsId);
            //如果库存已经为零了,那么后面进来的请求去访问redis是一种资源的浪费,为此我们在本地做一个线程标记
            //当该商品的库存为0时,标记该商品id的值为true,过来的请求如果在map里面,则不请求redis,直接进行拦截,减少redis的访问
            emptyStockMap.put(goodsId, true);
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }
        //用户是否重复抢购
        SeckillOrder seckillOrder =
                ((SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId));
        if(seckillOrder != null) {
            log.warn("用户{}在{}时刻重复抢购商品：{}", user.toString(), new Date(), goodsId);
            return RespBean.error(RespBeanEnum.REPEATE_ERROR);
        }
        //下单操作改成rabbitmq异步处理,这里使用自己写的JsonUtil工具转一下数据类型
        mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(new SeckillMessage(user, goodsId)));
        // 0 表示排队中
        return RespBean.success(0);
    }

    /**
     * 做接口调用隐藏，其实是在调接口的时候多了一个验证串
     * 在业务接口中，拼接该串，并存储到redis中做后续校验
     * 接口限流：
     *  其实还是利用redis计数的方式，设置一个key存活时间60s，每访问一次该key的值+1，时间到了清零
     *  后端判断值大于100（自己设定每分钟请求量）时返回系统繁忙，避免服务器宕机
     *
     *  captcha 验证码校验，这里不做实现，但是一般这里需要有验证码校验功能
     *
     *  通过AccessLimit自定义注解实现通用的秒杀处理
     * @param user
     * @param goodsId
     * @return
     */
    @ResponseBody
    @ApiOperation("获取秒杀地址")
    @AccessLimit(second = 5, maxCount = 5, needLogin = true)
    @RequestMapping(value = "/path", method = RequestMethod.GET)
    public RespBean getPath(User user,
                            //required 请求参数是否是必须的
                            @ApiParam(name = "商品ID(goodsId)", value = "商品id+用户id生成唯一接口", required = true) Long goodsId,
                            @ApiParam(name = "验证码(captcha)", value = "用于校验用户身份", required = true) String captcha,
                            HttpServletRequest request) {
        if (user == null) {
            return RespBean.error(RespBeanEnum.ERROR);
        }
        //接口限流,5s内访问5次,这里将限流优化成注解的形式，可看配置类的方法拦截器
//        ValueOperations valueOperations = redisTemplate.opsForValue();
//        String uri = request.getRequestURI();
//        Integer count = (Integer) valueOperations.get("interfaceCurrentLimiting:" + uri + ":" + user.getId());
//        if(count == null) {
//            valueOperations.set("interfaceCurrentLimiting:" + uri + ":" + user.getId(), 0,
//                    5, TimeUnit.SECONDS);
//        } else if(count < 5) {
//            valueOperations.increment("interfaceCurrentLimiting:" + uri + ":" + user.getId());
//        } else {
//            return RespBean.error(RespBeanEnum.VISIT_BUSY);
//        }
        //获取秒杀地址
        String path = orderService.createSeckillPath(user, goodsId);
        return RespBean.success(path);
    }


    //为了方便做案例，这里通过实现InitializingBean的afterPropertiesSet方法来实现数据初始化加载
    //系统初始化，在项目初始化的时候把商品库存加在到redis里面去
    @Override
    public void afterPropertiesSet() throws Exception {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        List<GoodsVo> allGoodsVo = goodsService.findAllGoodsVo();
        if(CollectionUtils.isEmpty(allGoodsVo)) {
            return;
        }
        allGoodsVo.forEach(goodsVo -> {
            emptyStockMap.put(goodsVo.getId(), false);
            valueOperations.set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
        });
    }


}
